/*
 * HTTPDialog.java
 *
 * Created on September 5, 2006, 10:19 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */
package smfbackup.http;

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;
import net.htmlparser.jericho.Element;
import net.htmlparser.jericho.Source;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.log4j.Logger;
import smfbackup.Main;
import smfbackup.Utils;
import smfbackup.conf.Configuration;
import smfbackup.conf.Forum;
import smfbackup.run.Manageable;

/**
 *
 * @author Iulian
 */
public class SMFConnection extends Manageable {

    private Logger log = Logger.getLogger(SMFConnection.class);
    // configures this connection
    private Properties config;
    // smf cookies seem not to work with httpclient, so i'll handle them manually
    private String cookiePol = CookiePolicy.IGNORE_COOKIES;
    // ar we running or actively downloading something ?
    private boolean running;
    // da forum
    private Forum forum;
    // current file name
    private String currentFileName;
    // cookie container
    SMFCookieContainer cc;
    // http client - one for each SMFConnection;
    HttpClient client;
    // connection state
    int state;
    // date formatter
    DateFormat sdf;
    HttpMethod currentMethod;

    /** Creates a new instance of SMFConnection */
    public SMFConnection(Forum forum) {
        config = new Properties();

        forum.updateDownloadCount(0);
        Main.getMainWindow().fireLogUpdate();

        /* TODO must select proxy and port if needed */
        config.setProperty("forum.url", forum.getUrl());
        config.setProperty("user.name", forum.getUsername());
        config.setProperty("user.pass", forum.getPass());

        this.setName(forum.getName() + "_thread");

        this.forum = forum;

        client = null;
        currentMethod = null;

        sdf = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.MEDIUM, SimpleDateFormat.MEDIUM);

        cc = new SMFCookieContainer();
        state = SMFState.NOT_STARTED;
    }

    /*
     * Does a Login on the forum, returns the admin URL or null of something else went wrong.
     */
    private String doLogin() throws Throwable {

        client = new HttpClient();
        cc.resetCookies();

        if (Configuration.isProxyEnabled()) {
            client.getHostConfiguration().setProxy(Configuration.getProxyHost(), Integer.parseInt(Configuration.getProxyPort()));
            forum.updateStatus("HTTP proxy enabled: [" + Configuration.getProxyHost() + ":" + Configuration.getProxyPort() + "]");
        }

        // set client parameters
        client.getParams().setParameter("http.protocol.allow-circular-redirects", Boolean.TRUE);
        client.getParams().setParameter("http.useragent", Configuration.getUserAgent());
        log.debug("Using User-Agent: " + Configuration.getUserAgent());

        currentMethod = new GetMethod(config.getProperty("forum.url") + "?action=admin");

        // cookie policy
        currentMethod.getParams().setCookiePolicy(cookiePol);

        if (!running && state != SMFState.ABORTING_DOWNLOAD) {
            return null;
        }

        forum.updateStatus("Connecting to admin backend.");
        log.info("Connecting to " + config.getProperty("forum.url") + "?action=admin");
        int statusCode = client.executeMethod(currentMethod);

        if (statusCode != HttpStatus.SC_OK) {
            log.error("Connect failed: " + currentMethod.getURI() + " said " + currentMethod.getStatusLine());
            throw new SMFException("Connect failed: " + currentMethod.getStatusLine());
        }

        cc.saveCookies(currentMethod.getResponseHeaders());

        // search first form which has login2 as action
        Source source = new Source(currentMethod.getResponseBodyAsStream());

        Element loginForm = null;
        List elements = source.findAllElements("form");
        for (Iterator i = elements.listIterator(); i.hasNext();) {
            Element form = (Element) i.next();
            String action = form.getAttributeValue("action");
            if (action != null && action.indexOf("login2") != -1) {
                loginForm = form;
                break;
            }
        }

        currentMethod.releaseConnection();

        if (loginForm == null) {
            throw new SMFException("No <form> tag whith action pointing to login2 (URL not pointing to SMF?).");
        }

        // asume post anyway
        currentMethod = new PostMethod(loginForm.getAttributeValue("action"));

        currentMethod.getParams().setCookiePolicy(cookiePol);
        cc.loadCookies(currentMethod);
        ((PostMethod) currentMethod).addParameter("user", config.getProperty("user.name"));
        ((PostMethod) currentMethod).addParameter("passwrd", config.getProperty("user.pass"));
        // one hour
        ((PostMethod) currentMethod).addParameter("cookielength", "60");
        if (forum.getVersion() == 2) {
            ((PostMethod) currentMethod).addParameter("hash_passwrd", "");
        }

        //TODO - 
        //admin_pass=********&sc=c49b1ca93abf1c1dca7f104a543e6003&admin_hash_pass=5c82632ec19114f6c859a3ceda271390b1f026c5

        if (!running && state != SMFState.ABORTING_DOWNLOAD) {
            return null;
        }

        forum.updateStatus("Sending password...");
        log.info("Issuing POST to " + loginForm.getAttributeValue("action"));
        statusCode = client.executeMethod(currentMethod);

        if (statusCode != HttpStatus.SC_OK && statusCode != HttpStatus.SC_MOVED_TEMPORARILY) {
            forum.updateStatus("Method failed (loginSubmit): " + currentMethod.getStatusLine());
        }

        cc.saveCookies(currentMethod.getResponseHeaders());

        // find the redirect.
        String url = null;
        if (statusCode == HttpStatus.SC_OK) {
            Header redirectHeader = currentMethod.getResponseHeader("Refresh");
            if (redirectHeader == null) {
                return null;
            }
            String redirectValue = redirectHeader.getValue();
            url = redirectValue.substring(redirectValue.indexOf("URL=") + 4);
        } else {
            Header redirectHeader = currentMethod.getResponseHeader("Location");
            url = redirectHeader.getValue();
        }

        currentMethod.releaseConnection();

        return url;

    }

    /*
     * Does all the steps needed to obtain the backup
     */
    private void doBackup() throws Throwable {

        state = SMFState.CONNECTION_IN_PROGRESS;

        String url = doLogin();

        if (url == null) {
            throw new SMFException("Login failed (no redirect received to the forum's admin page).");
        }

        // get the admin page now

        currentMethod = new GetMethod(url);
        currentMethod.setFollowRedirects(true);
        currentMethod.getParams().setCookiePolicy(cookiePol);
        cc.loadCookies(currentMethod);

        forum.updateStatus("Attempting to load the admin page.");
        log.info("Issuing GET on " + url);

        if (!running && state != SMFState.ABORTING_DOWNLOAD) {
            return;
        }
        int statusCode = client.executeMethod(currentMethod);

        if (statusCode != HttpStatus.SC_OK) {
            forum.updateStatus("Method failed (loginCheck): " + currentMethod.getStatusLine());
        }

        cc.saveCookies(currentMethod.getResponseHeaders());

        // on SMF 2.0 RC1, the sesc is not called sesc anymore, but is a random number
        String sesc = null;
        String sescName = null;

        /* For SMF 2.0 Beta 3 or SMF 2.0 RC1.X or SMF 2.0 RC2.X */
        /* 2 means second element from the dropdown */
        if (forum.getVersion() == 2 || forum.getVersion() == 3 || forum.getVersion() == 4) {
            Source source = new Source(currentMethod.getResponseBodyAsStream());

            // find sesc
            List inputs = source.findAllElements("a");
            for (Iterator i = inputs.listIterator(); i.hasNext();) {
                Element e = (Element) i.next();
                String hrefValue = e.getAttributeValue("href");
                if (hrefValue == null) {
                    continue;
                }

                // for 2.0 Beta 3
                if (forum.getVersion() == 2) {
                    int pos = hrefValue.indexOf("sesc");
                    if (pos > 0) {
                        sesc = hrefValue.substring(pos + 5);
                        break;
                    }
                } // for 2.0 RC 1.X
                else if (forum.getVersion() == 3 || forum.getVersion() == 4) {
                    int pos = hrefValue.indexOf("maintain");
                    if (pos > 0) {
                        StringTokenizer st = new StringTokenizer(hrefValue, ";");
                        while (st.hasMoreTokens()) {
                            String token = st.nextToken();
                            log.debug("Found a token: " + token);
                            if (token.startsWith("sa")) {
                                continue;
                            } else if (token.startsWith("sesc")) {
                                continue;
                            } else if (token.startsWith("area")) {
                                continue;
                            } else if (token.startsWith("http://")) {
                                continue;
                            } else if (token.startsWith("activity")) { // added for SMF RC2.X
                                continue;
                            } else {
                                sescName = token.substring(0, token.indexOf("="));
                                sesc = token.substring(token.indexOf("=") + 1);
                                log.debug("Found sescName " + sescName + ", sesc " + sesc);
                            }
                        }
                    }
                }
            }
        }

        currentMethod.releaseConnection();

        // click on maintain , we need secs parameter.
        String maintainUrl = null;
        // 2 means the third element in the dropdown
        switch (forum.getVersion()) {
            case 0:
            case 1:
            default:
                maintainUrl = config.getProperty("forum.url") + "?action=maintain";
                break;
            case 2:
                maintainUrl = config.getProperty("forum.url") + "?action=admin;area=maintain;sa=admintask;activity=maintain_backup;sesc=" + sesc;
                break;
            case 3:
                maintainUrl = config.getProperty("forum.url") + "?action=admin;area=maintain;sa=database;" + sescName + "=" + sesc;
                break;
            case 4:
                maintainUrl = config.getProperty("forum.url") + "?action=admin;area=maintain;sa=database";
                break;
        }

        currentMethod = new GetMethod(maintainUrl);
        currentMethod.getParams().setCookiePolicy(cookiePol);
        cc.loadCookies(currentMethod);

        forum.updateStatus("Got a redirect. Checking if password was ok...");
        log.info("Maintain: POST to " + maintainUrl);

        if (!running && state != SMFState.ABORTING_DOWNLOAD) {
            return;
        }
        statusCode = client.executeMethod(currentMethod);

        if (statusCode != HttpStatus.SC_OK) {
            forum.updateStatus("Method failed (maintainAction): " + currentMethod.getStatusLine());
        }

        cc.saveCookies(currentMethod.getResponseHeaders());

        Source source = new Source(currentMethod.getResponseBodyAsStream());
        // find sesc


        List inputs = source.findAllElements("input");
        for (Iterator i = inputs.listIterator(); i.hasNext();) {
            Element e = (Element) i.next();
            log.debug("name=" + e.getAttributeValue("name"));
            if ("sesc".equals(e.getAttributeValue("name"))) {
                sesc = e.getAttributeValue("value");
                break;
            }
            if ("sc".equals(e.getAttributeValue("name"))) {
                sesc = e.getAttributeValue("value");
                break;
            }
        }

        currentMethod.releaseConnection();

        if (sesc == null) {
            log.error("No \"sesc\" or \"sc\" input field found in the reply page. Cannot proceed.");
            throw new SMFException("Access denied (have you correctly chosen the forum version?).");
        }

        // let's execute the download database action!

        String compressAction = forum.isDoCompression() ? "&compress=gzip" : "";

        if (forum.getVersion() == 2) {
            url = maintainUrl;
            currentMethod = new PostMethod(url);
            ((PostMethod) currentMethod).addParameter("data", "on");
            if (forum.isDoCompression()) {
                ((PostMethod) currentMethod).addParameter("compress", "gzip");
            }
            ((PostMethod) currentMethod).addParameter("sc", sesc);
            ((PostMethod) currentMethod).addParameter("do", "1");
        } else if (forum.getVersion() == 3 || forum.getVersion() == 4) {
            url = maintainUrl + ";activity=backup";
            currentMethod = new PostMethod(url);
            ((PostMethod) currentMethod).addParameter("struct", "on");
            ((PostMethod) currentMethod).addParameter("data", "on");
            if (forum.isDoCompression()) {
                ((PostMethod) currentMethod).addParameter("compress", "gzip");
            }
            ((PostMethod) currentMethod).addParameter(sescName, sesc);
        } else {
            url = config.getProperty("forum.url") + "?struct=on&data=on" + compressAction + "&action=dumpdb&sesc=" + sesc;
            currentMethod = new GetMethod(url);
        }

        currentMethod.getParams().setCookiePolicy(cookiePol);
        cc.loadCookies(currentMethod);
        currentMethod.setRequestHeader("Accept-Encoding", "gzip,deflate");

        forum.updateStatus("Requesting the backup file.");
        log.info("Proceeding to backup. URL: " + url);

        if (!running && state != SMFState.ABORTING_DOWNLOAD) {
            return;
        }
        statusCode = client.executeMethod(currentMethod);
        if (statusCode != HttpStatus.SC_OK) {
            forum.updateStatus("Method failed (downloadAction): " + currentMethod.getStatusLine());
        }

        cc.saveCookies(currentMethod.getResponseHeaders());

        forum.updateStatus("Downloading...");

        long start_ = System.currentTimeMillis();
        long bytesRead = 0;

        currentFileName = composeBackupFileName();
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(currentFileName));
        InputStream is = currentMethod.getResponseBodyAsStream();
        byte content[] = new byte[65000];

        for (; true;) {
            int howMany = is.read(content);
            if (howMany == -1) {
                break;
            } else {
                bos.write(content, 0, howMany);
            }
            bytesRead += howMany;

            forum.updateDownloadCount(bytesRead);
            Main.getMainWindow().fireLogUpdate();
        }

        bos.close();

        currentFileName = null;

        currentMethod.releaseConnection();

        long end_ = System.currentTimeMillis();
        String speed = "";

        // less than one second ?
        if (end_ < (start_ + 1000)) {
            speed = "more than " + bytesRead / 1000;
        } else {
            speed = "" + bytesRead / (end_ - start_);
        }

        forum.updateStatus("Download complete. [" + speed + " kb/s]");
    }

    private String composeBackupFileName() {
        SimpleDateFormat fileNameDateFormat = new SimpleDateFormat("ddMMMyy_kkmmss");
        String path = Configuration.getBackupDir() + System.getProperty("file.separator") + forum.getName() + "_" + fileNameDateFormat.format(new Date()) + ".sql";
        if (forum.isDoCompression()) {
            path += ".gz";
        }

        forum.updateStatus("Using file: " + path);

        return path;
    }

    public void signal(int signal) {

        switch (signal) {
            case SMFSignal.SHUTDOWN:
                state = SMFState.SHUTTING_DOWN;
                running = false;
                abortDownload();
                interrupt();
                break;
            case SMFSignal.RUN_FORCED_BACKUP:
                if (state == SMFState.WAITING_FOR_NEXT_RUN) {
                    state = SMFState.FORCED_BACKUP;
                    interrupt();
                } else {
                    forum.updateStatus("Backup is already running. Please stop it or wait to finish.");
                }
                break;
            case SMFSignal.ABORT_DOWNLOAD:
                if (state == SMFState.CONNECTION_IN_PROGRESS) {
                    state = SMFState.ABORTING_DOWNLOAD;
                    abortDownload();
                } else {
                    forum.updateStatus("Connection is already idle. Nothing to stop.");
                }
                break;
            case SMFSignal.RELOAD_CONFIG:
                state = SMFState.RELOADING_CONFIG;
                abortDownload();
                interrupt();
                break;
            default:
                break;
        }
    }

    @Override
    public void doJob() {
        running = true;
        state = SMFState.IDLE;
        boolean firstRun = true;

        for (; running;) {
            try {

                // handle first run on program start
                if (firstRun) {
                    firstRun = false;
                    if (forum.isDoBackupWhenForumStarts() && !forum.isDisableBackups()) {
                        forum.updateStatus("Forum set to do backup upon program startup.");
                        doBackup();
                    }
                }

                try {
                    //  wait for next run
                    waitForNextRun();
                } catch (InterruptedException ioe) {
                }

                // check transitions that requested interrupting the wait
                switch (state) {
                    case SMFState.SHUTTING_DOWN:
                        forum.updateStatus("Shutting down the connection.");
                        break; // it will exit the loop anyway.
                    case SMFState.RELOADING_CONFIG:
                        forum.updateStatus("Reloading the forum configuration.");
                        break;
                    case SMFState.FORCED_BACKUP:
                        forum.updateStatus("Backup notification received.");
                    default:
                        // do backup if we are still running
                        forum.updateStatus("Starting backup procedure.");
                        doBackup();
                }

                // check transitions that requested aborting the download
                switch (state) {
                    // just go to wait
                    case SMFState.ABORTING_DOWNLOAD:
                        forum.updateStatus("Aborted the download.");
                        break;
                    case SMFState.RELOADING_CONFIG:
                        reloadConfig();
                        forum.updateStatus("Configuration reloaded");
                        break;
                    case SMFState.SHUTTING_DOWN:
                        break; // running is also false, this will exit the for loop;
                    default:
                        break;
                }

            } catch (Throwable t) {
                log.error("Exception occured: ", t);
                state = SMFState.IDLE;
                forum.updateStatus(Utils.getStackTraceAsString(t));
            }
        }
    }

    private void waitForNextRun() throws InterruptedException {

        state = SMFState.WAITING_FOR_NEXT_RUN;

        if (!forum.isDisableBackups()) {
            GregorianCalendar nextRun = getTimeOfNextRun();
            forum.updateStatus("Next run at: " + sdf.format(nextRun.getTime()));
            Thread.sleep(nextRun.getTimeInMillis() - (new Date()).getTime());
        } else {
            forum.updateStatus("Automatic backups disabled for this forum. Sleeping...");
            for (; running;) {
                // sleeping 1 minute
                Thread.sleep(60000);
            }
        }
    }

    private GregorianCalendar getTimeOfNextRun() {
        String hourminute = forum.getStartAt();
        int repeat = Integer.parseInt(forum.getRepeatEvery());

        GregorianCalendar now = new GregorianCalendar();

        StringTokenizer st = new StringTokenizer(hourminute, ":");
        int hour = Integer.parseInt(st.nextToken());
        int minute = Integer.parseInt(st.nextToken());

        GregorianCalendar firstRun = new GregorianCalendar();
        firstRun.set(Calendar.SECOND, 0);
        firstRun.set(Calendar.MILLISECOND, 0);
        firstRun.set(Calendar.HOUR_OF_DAY, hour);
        firstRun.set(Calendar.MINUTE, minute);

        while (firstRun.before(now)) {
            firstRun.add(Calendar.HOUR_OF_DAY, repeat);
        }

        return firstRun;
    }

    private void abortDownload() {
        if (currentMethod != null) {
            forum.updateStatus("Aborting connection. Terminating active connection.");
            currentMethod.abort();
            currentMethod = null;
        } else {
            forum.updateStatus("Connection is already idle. Nothing to stop.");
        }
    }

    @Override
    public void shutdown() {
        signal(SMFSignal.SHUTDOWN);
    }

    private void reloadConfig() {
        config.setProperty("forum.url", forum.getUrl());
        config.setProperty("user.name", forum.getUsername());
        config.setProperty("user.pass", forum.getPass());

        this.setName(forum.getName() + "_thread");
    }
}
